今天是 API登入權限控管機制
系列最後一天,昨天已經建立好帶有權限的API了,今天我們就要來修改前端的功能!!
將登入機制導入並且修改介接的API,最後紀錄登入的資訊,這一部份就完成了!
創建一個登入頁 WebLogin.aspx
(假設一律要登入才能使用),讓使用者輸入帳號密碼進行登入,使用aspx是因為可以讓帳號驗證function runat="server"
code behind
寫在後端 "WebLogin.aspx.cs"
。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebLogin.aspx.cs" Inherits="OLMap.WebLogin" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>OLMap Demo登入</title>
<link rel="stylesheet" href="css/login.css">
</head>
<body class="login-page">
<form id="form1" runat="server" defaultbutton="LoginBtn">
<div class="center">
<div class="title">
OLMap Demo
</div>
<div class="line"></div>
<div id="inputs">
<table id="otherLogin">
<tr>
<td colspan="2" class="login-hint">請輸入帳號密碼</td>
</tr>
<tr>
<td>帳號</td>
<td>
<asp:TextBox ID="txt_user" placeholder="請輸入帳號" runat="server">
</asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2" align="right">
<asp:RequiredFieldValidator ID="UserNameRequired" runat="server" ControlToValidate="txt_user" ErrorMessage="*帳號必填" ToolTip="必須提供使用者名稱。" ValidationGroup="Login2" CssClass="warning-text">
</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>密碼</td>
<td>
<asp:TextBox ID="txt_pass" placeholder="請輸入密碼" runat="server" TextMode="Password"></asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2" align="right">
<asp:RequiredFieldValidator ID="PasswordRequired" runat="server" ControlToValidate="txt_pass" ErrorMessage="*密碼必填" ToolTip="必須提供密碼。" ValidationGroup="Login2" CssClass="warning-text">
</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td colspan="2">
<asp:Button ID="LoginBtn" Text="登入" CssClass="button-act" runat="server" ValidationGroup="Login2" OnClick="LoginBtn_Click" />
</td>
</tr>
</table>
</div>
</div>
</form>
</body>
</html>
登入頁樣式寫在 css/login.css
,給它一個漂漂亮亮的畫面。
html,body {
position: relative;
width: 100%;
height: 100%;
padding: 0;
min-height: 600px;
margin: 0;
overflow: auto;
font-family: microsoft jhenghei;
}
body {
background: linear-gradient( 135deg, #2af598, #009efd);
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
}
table {
width: 100%;
margin-bottom: 40px;
}
table:last-child {
margin-bottom: 0;
}
.center {
position: absolute;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
top: 50%;
transform: translate(0, -50%);
width: 500px;
background-color: rgba(0, 0, 0, 0.35);
border-radius: 5px;
padding-bottom: 30px;
}
#inputs {
padding: 20px 50px;
color: white;
}
#inputs input {
border: none;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
color: white;
width: 100%;
box-sizing: border-box;
font-family: microsoft jhenghei;
}
::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: rgba(255, 255, 255, 0.7);
opacity: 1; /* Firefox */
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: rgba(255, 255, 255, 0.7);
}
::-ms-input-placeholder {
/* Microsoft Edge */
color: rgba(255, 255, 255, 0.7);
}
.title {
text-align: center;
padding: 30px;
padding-bottom: 10px;
color: white;
font-size: 30px;
font-weight:800;
}
.line {
width: 80%;
height: 1px;
background: linear-gradient(135deg, #00eaff, #0047ff);
position: relative;
left: 50%;
margin-left: -40%;
}
.button-act {
background: linear-gradient(135deg, #04befe, #4481eb);
padding: 15px !important;
font-size: 20px;
cursor: pointer;
box-shadow: 5px 5px 25px rgba(0, 0, 0, 0.2);
transition: 0.5s;
}
.button-act:hover {
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5);
}
.warning-text {
font-size: 14px;
color: #fba503;
}
.login-hint {
font-weight: bold;
padding-bottom: 16px;
opacity: 0.8;
/*text-decoration: underline;*/
}
登入頁示意圖:
在登入頁輸入帳號密碼,經過驗證成功後,會回傳token並存入 session
,後續API都要靠這組token來進行驗證。
在 Web.config
新增API路徑網址
<appSettings>
<add key="OLMapAPI" value="http://localhost/OLMapAPI"/>
</appSettings>
WebLogin.aspx.cs
新增頁面載入 Page_Load
事件,登出並刪除所有 session (含token和username)
protected void Page_Load(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
Session["token"] = Session["username"] = null;
Session.RemoveAll();
}
WebLogin.aspx.cs
新增 validatesApiUser()
函式驗證帳號密碼,若驗證成功則回傳true,將token存入session;驗證失敗回傳false。
public static bool validatesApiUser(string userInput, string passwordInput)
{
string url = ConfigurationManager.AppSettings["OLMapAPI"] + "/api/Auth/validatesApiUser";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "application/json; charset=utf-8";
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
string json = "{\"userid\":\"" + userInput + "\",\"password\":\"" + passwordInput + "\"}";
streamWriter.Write(json);
}
//API回傳的字串
string responseStr = "";
//發出Request
HttpWebResponse res;
try
{
res = (HttpWebResponse)request.GetResponse();
}
catch (WebException ex)
{
res = (HttpWebResponse)ex.Response;
}
StreamReader sr = new StreamReader(res.GetResponseStream(), Encoding.UTF8);
responseStr = sr.ReadToEnd();
JObject obj = (JObject)JsonConvert.DeserializeObject(responseStr);
System.Web.HttpContext.Current.Session["token"] = Convert.ToString(obj["token"]);
if (Convert.ToString(obj["token"]) == "")
{
return false;
}
else
{
return true;
}
}
新增登入按鈕點擊事件 LoginBtn_Click()
,當按下登入按鈕時執行 validatesApiUser()
,塞回 isValid
。
isValid = true
,將 username
存入session,並將網頁導向 map.aspx
。isValid = false
,則 alert 帳號密碼錯誤
。protected void LoginBtn_Click(object sender, EventArgs e)
{
bool isValid = false;
string userInput = txt_user.Text.TrimEnd();
string passwordInput = txt_pass.Text.TrimEnd();
isValid = validatesApiUser(userInput, passwordInput);
Session["username"] = userInput;
if (isValid)
{
Response.Redirect("~/map.aspx");
}
else
{
Session["username"] = null;
this.Page.Controls.Add(new LiteralControl("<script language='javascript'>alert('帳號密碼錯誤');</script>"));
}
}
在首頁 map.aspx
將session存入 localStorage
方便存取,但是如果全部頁面都是ASP.NET架構,就不需要存在localStorage。
<script type="text/javascript">
localStorage["token"] = "<%=Session["token"]%>";
localStorage["username"] = "<%=Session["username"]%>";
</script>
修正 map.aspx.cs
,若沒有 Session
內沒有儲存 token
和 username
則跳轉至登入頁 WebLogin.aspx
。
protected void Page_Load(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(Session["token"]?.ToString()) || string.IsNullOrEmpty(Session["username"]?.ToString()))
{
Response.Redirect("~/WebLogin.aspx");
}
}
從 Web.config
內將首頁改成 WebLogin.aspx
,所有使用者都要先登入才可使用圖台。
<defaultDocument>
<files>
<add value="WebLogin.aspx" />
</files>
</defaultDocument>
改完上述以後,可以順利登入並取得token存入session,亦可顯示圖台畫面。
但是若要使用介接本次的Web API的相關功能,則會產生以下錯誤:已拒絕此要求的授權
,所以下面我們要更改介接API的 request
資訊。
修正使用api各支的 header
要帶入token才可順利存取,以 getlayer()
為例,需在request header 加入 "Authorization"
參數,值為先前存入 localStorage
的token:
$.ajax({
type: "GET",
url: config_OLMapWebAPI + "/Layers/getLayerResource",
headers: {
"Authorization": localStorage["token"]
},
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function (d) {
//var data = $.parseJSON(d.d);
var data = d;
console.log(data);
var layerlisthtml = "";
TOCArray = [];
$.each(data, function (index, item) {
loadLayer(item);
if (typehasExtent.includes(item.DataType)) {
layerlisthtml += '<div class="item"><div class="ui checkbox"><input type="checkbox" name="example" onclick="toc.toggleTocLayer(\'' + item.LayerID + '\', this)"><label></label></div><div class="content"><a class="header">' + item.LayerTitle + '</a><div class="description">' + item.DataType + '</div><img class="layerBtns info" src="images/TOCpage/info.png" title="點擊定位圖層" onclick="toc.zoomTocLayer(\'' + item.LayerID + '\')"></div></div>';
} else {
layerlisthtml += '<div class="item"><div class="ui checkbox"><input type="checkbox" name="example" onclick="toc.toggleTocLayer(\'' + item.LayerID + '\', this)"><label></label></div><div class="content"><a class="header">' + item.LayerTitle + '</a><div class="description">' + item.DataType + '</div></div></div>';
}
TOCArray.push(item);
});
$("#layerlist").html(layerlisthtml);
},
error: function (jqXHR, exception) {
ajaxError(jqXHR, exception);
}
});
可以由localStorage 看到token資訊,更改完成後即可順利介接API。
接著,為了讓使用者辨別方便,在頁面上顯示 使用者的ID
。
<a style="float: right;color: white;margin-top: 20px;margin-right: 20px;">使用者:<span id="useridshow"></span></a>
$("#useridshow").html(localStorage["username"]);
每一次的登入都應該有log紀錄,token的紀錄在超過有效期限後便會被清除,因此需要另外紀錄登入的使用者與時間資訊。
新增登入記錄資料表 dbo.CRM_Record
,包含登入類型、登入名稱、登入時間和備註。
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CRM_Record](
[item] [smallint] IDENTITY(1,1) NOT NULL,
[LoginType] [smallint] NULL,
[LoginName] [nvarchar](20) NULL,
[AccessTime] [smalldatetime] NULL,
[Remark] [nvarchar](100) NULL
) ON [PRIMARY]
GO
修改 OLMapAPI
裡建立token的api:createToken()
。
當建立token的同時記錄紀錄登入的資訊到 CRM_Record
資料表內。
private string createToken(string userid, string type)
{
string salt = GetRandomString(10);
string token = generateToken(userid, salt);
delExpiredToken();
string sqlstr = @"INSERT INTO sys_tokens (userid,type,token,ip,issuedOn,expiredOn,salt) Values (@userid,@type,@token,@ip,@issuedOn,@expiredOn,@salt)";
// 新增紀錄登入資訊
sqlstr += @"INSERT INTO CRM_Record (LoginName,AccessTime,Remark) Values (@userid,GETDATE(),'OLMap')";
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
SqlCommand cmd = new SqlCommand(sqlstr, conn);
conn.Open();
cmd.Parameters.AddWithValue("@userid", userid);
cmd.Parameters.AddWithValue("@type", type);
cmd.Parameters.AddWithValue("@token", token);
cmd.Parameters.AddWithValue("@ip", getClientIP());
cmd.Parameters.AddWithValue("@issuedOn", DateTime.Now);
cmd.Parameters.AddWithValue("@expiredOn", DateTime.Now.AddHours(8)); //8小時後刪除
cmd.Parameters.AddWithValue("@salt", salt);
SqlDataReader dr = cmd.ExecuteReader();
dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
return "OLMapAPI " + token;
}
所有的權限控管機制功能都完成拉!!!加起來前後耗時4天的時間,鐵人賽到今天第23天,我個人覺得已經可以完成一個WebGIS的網頁了。
目前介紹的權限控管為最簡單的只分成有權限和無權限,而各位在真正建立一個專案時可以考慮:
Anonymous
的token,一樣走token驗證機制,只是他的權限是Anonymous。有很多很多東西都可以玩,在有各種想法以後就可以慢慢地去學習去實現,這大概就是做side project的樂趣吧!
明天回歸WebGIS的部分!我們來學怎麼在地圖上繪圖~